Bicycle
是MountainBike
和RoadBike
的父類別,Bicycle
包含共同的行為,而MountainBike
和RoadBike
則用來增加特定行為。Bicycle
的公共介面包含了spares
和size
,而其子類別的介面才是增加各自的專屬零件。Bicycle
是抽象的,所以不會定義一輛完整的自行車,它只會包含所有自行車的共同內容。
抽象類別常作為父類別而存在,這是它們的唯一用途。它們提供了一個共同的行為集合,可以被一組子類別所共用,而這些子類別則提供特殊化行為。
建立新層次結構起手式:暫時先忽略程式碼的適當性,完成這項修改的最簡單作法,是將Bicycle
重新命名為RoadBike
,並重新建立一個空Bicycle
類別。
class Bicycle
# 這個類別現在為空。
# 所有程式碼都被移到RoadBike。
end
class RoadBike < Bicycle
# 現在是Bicycle的子頫別。
# 包含所有來白舊Bicycle類別的程式碼
end
class MountainBike<Bicycle
# 仍然是Bicycle(現在為空)的一個子類別。
# 程式碼未發生變化
end
新的RoadBike
類別被定義成了Bicycle
的子類別。既有的MountainBike
類別已經是Bicycle
的子類別。它的程式碼雖未經修改,但是其行為顯然改變了,原先MountainBike
所依賴的程式碼已經從其父類別裡移除,並被放到一個對等類別裡。
由於size
和spares
是所有自行車的基本配備,因此要將他抽到父類別,也就是Bicycle
類別,將size
行為提升至父類別需要做三項修改,具體內容如下面的範例所示:
attr_reader
從RoadBike
提升到Bicycle
initialize
程式碼從RoadBike
提升到Bicycle
RoadBike
的initialize
裡增加了一個super
傳送現在當RoadBike
接收到size
訊息,他會將任務委派給父類別,在Bicycle
去實作size
。
class Bicycle
attr_reader :size # <- 推取自 RoadBike
def initialize(args={})
@size - args[:size] # <-捅取自RoadBike
end
end
class RoadBike < Bicycle
attr_reader :tape_color
def initialize(args)
@tape_color = args[:tape_color]
super(args) # <- RoadBike現在必須傳送「super」
end
# ...
end
在修改之前,RoadBike
能正確地冋應size
,但MountainBike
不行。而現在它們的共同行為現在已經被定義在它們的父類別Bicycle
裡面。繼承的神奇之處在於,現在它們都可以像下面所示那樣正確地回應size
。
road_bike = RoadBike.new(
size: 'M',
tape_color: 'red')
road_bike.size #'M'
mountain_bike = Mountain.new(
size: 'S',
front_shock: 'Manitou',
rear_shock: 'Fox')
mountain_bike.size # -> 'S
請注意,這段處理自行車尺寸的程式碼已被移動「兩次」。它最初是在Bicycle
類別裡,後來被下移到了RoadBike
,而現在又被提升到Bicycle
。雖然這段程式碼沒有變化,但被移動「兩次」是具有意義的,為什麼不一開始就讓這段程式碼留在Bicycle
裡呢?這種 「先全部下放,再部分提升」 的策略是這項重構的重點。許多繼承的難處都是因爲未能嚴格區分具體與抽象而導致的。
如果從Bicycle
的第一個版本開始重構,並試圖將具體程式碼隔離起來然後下移到RoadBike
,那麼若是你有任何遺漏,都會在父類別裡遺留危險的具體殘餘。但如果從一開始便將Bicycle
的所有程式碼移到RoadBike
,那麼你就可以仔細地辨別出抽象部分並進行提升,並且無須擔心會有具體的部份殘留在父類別裡。
重構一個繼承層次結構的一般原則是 提升抽象 而不是將 具體下移 。
分離重點:抽象部份將被提升到Bicycle
,而具體部分則繼續留在RoadBike
裡。
先暫時不去考量整個spares
方法,集中精力來提升所有自行車都應共用的內容,即chain
和tire_size
。如同size
一樣,它們都是屬性,並且都應該使用attr_accessor
來表示,而非使用寫死的值。具體的要求如下:
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args={})
@size = args[:size]
@chain = args[:chain]
@tire_size = args[:tire_size]
end
def default_chain # <-共同的預設值
'10-speed'
end
end
class RoadBike < Bicycle
#...
def default_tire_size # <-子類別預設值
'23'
end
end
class MountainBike < Bicycle
#...
def default_tire_size # <-子類別預設值
'2.1'
end
end
將預設值封裝在方法裡,Bicycle
傳送這些訊息的主要目的是讓了類別有機會覆蓋它們,以提供特殊化。
MountainBike
和RoadBike
提供了它們自己的預設輪胎尺寸,但繼承了共同的鏈條預設值。
class Bicycle
attr_reader :size, :chain, :tire_size
def Initialize(args ={})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || defau1t_tire_size
end
def default_chain # <-共同的預設值
'10-speed'
end
end
class RoadBike < Bicycle
#...
def default_tire_size # <-子類別預設值
'23'
end
end
class MountainBike < Bicycle
#...
def default_tire_size # <-子類別預設值
'2.1'
end
end
road_bike = RoadBike.new(
size: 'M',
tape_color: 'red')
p road_bike.tire_size # => '23'
p road_bike.chain # => "10-speed"
mountain_bike = MountainBike.new(
size: 'S',
front_shock: 'Manitou',
rear_bike: 'Fox')
p mountain_bike.tire_size # => '2.1 14
p mountain_bike.chain # => "10-speed"
現在有一個新的情境:如果有某位程式設計師在不知情的情況下想要建立一個新的RecuirtoentBike
子類別, 卻忽略了 default_tire_size
實作,會產生以下錯誤:
class RecumbentBike < Bicycle
def default_chain
'9-speed'
end
end
bent = RecunibentBike.new
# NameError: undefined local variable or method 09
# ' default_tire_size'
class Bicycle
def default_tire_size
raise NotImplementedError
"This #{self.class} cannot respond to:"
end
end
bent = RecximbentBike.new
# NotlnplementedError:
# This RecumbentBike cannot respond to:
# 'default_tire_size'
spares
的第一種實作撰寫起來最為簡單,但也會產生最緊密耦合的類別。既然Bicycle
現在可以傳送訊息來取得鏈條和輪胎尺寸,並且其spares
實作應該傳回一個散列,那麼增加下面的spares
方法即可滿足MountainBike
的需求。
class Bicycle
#...
def spares
{tire_size: tire_size, chain: chain)
end
end
class RecumbentBike < Bicycle
attr_reader :flag
def initialize(args)
@flag = args[:flag] # 忘記傳送 super
end
def spares
super.merge({flag: flag})
end
def default_chain
'9-speed'
end
def default_tire_size
'28'
end
end
bent = RecumbentBike.new(flag:'tall and orange')
bent.spares
# -> {:tire_size => nil, <-未進行初始化
# :chain => nil,
# :flag => "tall and orange"}
當RecumbentBike
在執行initialization
期間未能傳送super
時,它便會漏掉由Bicycle
提供的共同初始化作業,因此無法取得有效的尺寸、鏈條和輪胎尺寸。
參考資料: